﻿using System;
using System.Collections.Generic;
using Microsoft.VisualStudio.Debugger.Interop;
using Red.CommUtils;
using Red.Scripts.Debugger.Defines;
using AD7 = Red.Scripts.Debugger.Engine;

namespace Red.Scripts.Debugger.Remote.Debugger
{
    public class Program : IProgram, IDebugProgram2
    {
        #region Public Types

        public enum BreakType
        {
            None,
            StepInto,
            StepOut,
            StepOver,
            Pause,
            Breakpoint,
        }

        #endregion Public Types

        #region Private Data

        private AD7.Breakpoint.Manager _pendingBreakpointsManager;

        private List<Thread> _threadPool = new List<Thread>();
        private int _threadCount;
        private uint _activeThreadId;

        #endregion Private Data

        #region Public Properties

        public Guid Guid { get; private set; }
        public IPostman Postman { get; private set; }
        public IChannel Channel { get; private set; }
        public BreakType RequestedBreakType { get; private set; } = BreakType.None;
        public IList<IDebugThread2> Threads => _threadPool.GetRange( 0, _threadCount ).ConvertAll( item => (IDebugThread2)item );

        public List<Breakpoint.Bound> BoundBreakpoints { get; private set; } = new List<Breakpoint.Bound>();

        public Remote.Program PortProgram { get; private set; }

        #endregion Public Properties

        #region C-Tor

        public Program( Guid guid, IPostman postman, Remote.Program program, AD7.Breakpoint.Manager pendingBreakpointsManager )
        {
            Guid = guid;
            Postman = postman;
            PortProgram = program;
            _pendingBreakpointsManager = pendingBreakpointsManager;
        }

        #endregion C-Tor

        #region Public Methods

        public bool Connect( int retries = 10 )
        {
            while ( ( Channel = PortProgram.ParentProcess.CreateChannel() ) == null && --retries > 0 ) ;

            if ( Channel == null )
                return false;

            Channel.RegisterListener( OnMessage );
            Channel.Disconnected += Channel_Disconnected;

            return true;
        }

        #endregion Public Methods

        #region Private Methods

        void UpdateThreadData( IList<comm.ScriptThread> threads )
        {
            ClearThreadData();

            _threadCount = threads.Count;
            while ( _threadPool.Count < _threadCount )
            {
                _threadPool.Add( new Thread( this ) );
            }

            for ( int i = 0; i < _threadCount; ++i )
            {
                comm.ScriptThread source = threads[ i ];
                Thread target = _threadPool[ i ];

                target.Data = source;

                Postman.SendThreadCreated( this, target );
            }
        }

        void ClearThreadData()
        {
            for ( int i = 0; i < _threadCount; ++i )
            {
                Postman.SendThreadDestroyed( this, _threadPool[ i ] );
            }
        }

        Thread GetThread( uint id )
        {
            for ( int i = 0; i < _threadCount; ++i )
            {
                Thread thread = _threadPool[ i ];

                if ( thread.Data.id == id )
                    return thread;
            }

            return null;
        }

        void IncrementBreakpointHitcount( comm.ScriptBreakpointRelative breakpoint )
        {
            Breakpoint.Location location = Breakpoint.Location.CreateFromRedEngine( breakpoint );
            Breakpoint.Bound boundBreakpoint = FindBreakpoint( location );

            if ( boundBreakpoint != null )
            {
                ++boundBreakpoint.HitCount;
            }
        }

        void HandleBreakpoint( Thread activeThread )
        {
            System.Diagnostics.Debug.Print( $"Handling breakpoint" );

            Breakpoint.Location location = Breakpoint.Location.CreateFromRedEngine( activeThread.Data.frames[ 0 ].breakpoint );
            Breakpoint.Bound breakpoint = FindBreakpoint( location );

            if ( breakpoint != null )
            {
                System.Diagnostics.Debug.Print( $"Bound breakpoint found" );
                ++breakpoint.HitCount;
                Postman.SendBreakpointHitEvent( this, activeThread, breakpoint );
            }
            else
            {
                System.Diagnostics.Debug.Print( $"No bound breakpoint found, attempting pause instead" );
                Postman.SendPauseCompleteEvent( this, activeThread );
            }
        }

        #endregion Private Methods

        #region Event Handlers

        private void OnMessage( comm.Message message, IResponse responseGateway )
        {
            System.Diagnostics.Debug.Print( $"Recieved message of type: {message.GetName()}" );

            if ( message.IsA( comm.EMessageID.ScriptBreakExecuted ) )
            {
                comm.ScriptBreakExecuted breakExecutedMessage = (comm.ScriptBreakExecuted)message;

                _activeThreadId = breakExecutedMessage.breakpointThreadId;
                UpdateThreadData( breakExecutedMessage.threads );
                Thread activeThread = GetThread( _activeThreadId );

                System.Diagnostics.Debug.Print( $"Recieved breakpoint message for thread: {breakExecutedMessage.breakpointThreadId}, RequestedBreakType: {RequestedBreakType}, Reason: {breakExecutedMessage.reason}" );

                switch ( RequestedBreakType )
                {
                    case BreakType.StepInto:
                    case BreakType.StepOut:
                    case BreakType.StepOver:
                        Postman.SendStepCompleteEvent( this, activeThread );
                        IncrementBreakpointHitcount( activeThread.Data.frames[ 0 ].breakpoint );
                        break;

                    case BreakType.Pause:
                        Postman.SendPauseCompleteEvent( this, activeThread );
                        IncrementBreakpointHitcount( activeThread.Data.frames[ 0 ].breakpoint );
                        break;

                    default:
                        if ( breakExecutedMessage.reason == "Breakpoint" )
                        {
                            HandleBreakpoint( activeThread );
                        }
                        break;
                }

                RequestedBreakType = BreakType.None;
            }
            else if ( message.IsA( comm.EMessageID.ScriptBreakpointSkipped ) )
            {
                comm.ScriptBreakpointSkipped skippedMessage = (comm.ScriptBreakpointSkipped)message;
                IncrementBreakpointHitcount( skippedMessage.breakpoint );
            }
            else if ( message.IsA( comm.EMessageID.ScriptBinaryReloaded ) )
            {
                _pendingBreakpointsManager.Rebind();
            }
        }

        private void Channel_Disconnected()
        {
            Channel.UnregisterListener( OnMessage );
            Postman.SendProgramDestroyedEvent( this );
        }

        #endregion Event Handlers

        #region IProgram

        public void BindBreakpoint( IDebugPendingBreakpoint2 pending, bool enabled, BreakpointBindCallback callback )
        {
            Breakpoint.Bound.BoundEvent onBound = null;
            Breakpoint.Bound.BoundErrorEvent onError = null;

            onBound = ( Breakpoint.Bound bound ) =>
            {
                bound.IsEnabled = enabled;
                bound.BindSuccessful -= onBound;
                bound.Error -= onError;
                bound.Deleted += Breakpoint_Deleted;
                BoundBreakpoints.Add( bound );

                callback( bound );
            };

            onError = ( Breakpoint.Bound bound, Defines.Breakpoint.Error error ) =>
            {
                bound.BindSuccessful -= onBound;
                bound.Error -= onError;

                callback( error );
            };


            Breakpoint.Bound breakpoint = new Breakpoint.Bound( pending, this, Channel, Postman );
            breakpoint.BindSuccessful += onBound;
            breakpoint.Error += onError;

            breakpoint.StartBind();
        }

        internal Breakpoint.Bound FindBreakpoint( Breakpoint.Location location )
        {
            foreach ( Breakpoint.Bound breakpoint in BoundBreakpoints )
            {
                if ( breakpoint.CheckConfirmed( location ) )
                    return breakpoint;
            }

            return null;
        }

        private void Breakpoint_Deleted( Breakpoint.Bound breakpoint )
        {
            breakpoint.Deleted -= Breakpoint_Deleted;
            BoundBreakpoints.Remove( breakpoint );
        }

        #endregion IProgram

        #region IDebugProgram2

        public int CanDetach()
        {
            return Constants.S_OK;
        }

        public int CauseBreak()
        {
            RequestedBreakType = BreakType.Pause;

            comm.ScriptBreakRequest message = new comm.ScriptBreakRequest();
            message.type = RequestedBreakType.ToString();

            Channel.Send( message );

            return Constants.S_OK;
        }

        public int Continue( IDebugThread2 pThread )
        {
            return Constants.S_OK;
        }

        public int Detach()
        {
            Channel.UnregisterListener( OnMessage );
            Channel.ShutDown();

            Postman.SendProgramDestroyedEvent( this );

            return Constants.S_OK;
        }

        public int EnumCodeContexts( IDebugDocumentPosition2 pDocPos, out IEnumDebugCodeContexts2 ppEnum )
        {
            throw new NotImplementedException();
        }

        public int EnumCodePaths( string pszHint, IDebugCodeContext2 pStart, IDebugStackFrame2 pFrame, int fSource, out IEnumCodePaths2 ppEnum, out IDebugCodeContext2 ppSafety )
        {
            throw new NotImplementedException();
        }

        public int EnumModules( out IEnumDebugModules2 ppEnum )
        {
            ppEnum = null;

            return Constants.E_NOTIMPL;
        }

        public int EnumThreads( out IEnumDebugThreads2 ppEnum )
        {
            ppEnum = new Defines.Enumerators.Thread( Threads );

            return Constants.S_OK;
        }

        public int GetDebugProperty( out IDebugProperty2 ppProperty )
        {
            throw new NotImplementedException();
        }

        public int GetDisassemblyStream( enum_DISASSEMBLY_STREAM_SCOPE dwScope, IDebugCodeContext2 pCodeContext, out IDebugDisassemblyStream2 ppDisassemblyStream )
        {
            throw new NotImplementedException();
        }

        public int GetENCUpdate( out object ppUpdate )
        {
            throw new NotImplementedException();
        }

        public int GetEngineInfo( out string pbstrEngine, out Guid pguidEngine )
        {
            pbstrEngine = null;
            pguidEngine = Defines.Id.Guid.Engine;

            return Constants.S_OK;
        }

        public int GetMemoryBytes( out IDebugMemoryBytes2 ppMemoryBytes )
        {
            throw new NotImplementedException();
        }

        public int GetName( out string pbstrName )
        {
            pbstrName = "Red Scripts Thread";

            return Constants.S_OK;
        }

        public int GetProgramId( out Guid pguidProgramId )
        {
            pguidProgramId = Guid;

            return Constants.S_OK;
        }

        public int Step( IDebugThread2 pThread, enum_STEPKIND sk, enum_STEPUNIT unit )
        {
            comm.ScriptBreakRequest message = new comm.ScriptBreakRequest();

            switch ( sk )
            {
                case enum_STEPKIND.STEP_INTO:
                    RequestedBreakType = BreakType.StepInto;
                    break;

                case enum_STEPKIND.STEP_OUT:
                    RequestedBreakType = BreakType.StepOut;
                    break;

                case enum_STEPKIND.STEP_OVER:
                    RequestedBreakType = BreakType.StepOver;
                    break;
            }

            message.type = RequestedBreakType.ToString();
            int result = pThread.GetThreadId( out message.breakpointThreadId );
            System.Diagnostics.Debug.Assert( result == Constants.S_OK );

            Channel.Send( message );

            return Constants.S_OK;
        }

        public int Terminate()
        {
            if ( PortProgram != null )
            {
                Detach();

                IDebugPortNotify2 portNotify = (IDebugPortNotify2)PortProgram.ParentProcess.Parent;
                portNotify.RemoveProgramNode( PortProgram );
                PortProgram = null;
            }

            return Constants.S_OK;
        }

        public int WriteDump( enum_DUMPTYPE DUMPTYPE, string pszDumpUrl )
        {
            throw new NotImplementedException();
        }

        #endregion IDebugProgram2

        #region Deprecated - Not called by the debugger

        public int Attach( IDebugEventCallback2 pCallback )
        {
            throw new NotImplementedException();
        }

        public int GetProcess( out IDebugProcess2 ppProcess )
        {
            throw new NotImplementedException();
        }

        public int Execute()
        {
            comm.ScriptResumeExecution message = new comm.ScriptResumeExecution();

            Channel.Send( message );
            ClearThreadData();

            return Constants.S_OK;
        }

        #endregion Deprecated - Not called by the debugger
    }
}
